package com.opennews.openplatform.myspringbootcore.security;

import com.opennews.openplatform.myspringbootcore.dto.RequestMap;
import com.opennews.openplatform.myspringbootcore.util.SharedUtil;
import org.springframework.core.env.Environment;

public class AuthRequestValidator {
    private final Environment env;
    private final RequestMapManager requestMapManager;
    private final ActiveTokenManager activeTokenManager;
    private final TokenManager tokenManager;
    private final String apiPathPrefix;

    public AuthRequestValidator(Environment env, RequestMapManager requestMapManager, ActiveTokenManager activeTokenManager, TokenManager tokenManager, String apiPathPrefix) {
        this.env = env;
        this.requestMapManager = requestMapManager;
        this.activeTokenManager = activeTokenManager;
        this.tokenManager = tokenManager;
        this.apiPathPrefix = apiPathPrefix;
    }

    public boolean checkAccessPermission(String requestMethod, String requestURI, String requestContextPath, String authorizationHeader) {
        // FullPath returns a matched route full path.
        // 404 returns an empty string.
        String urlFullPath = requestURI;
        boolean granted = false;

        if (!SharedUtil.isNullOrEmpty(urlFullPath)) {
            if (!urlFullPath.toLowerCase().endsWith("error")) {
                // Gets authorization from request header.
                // The string should be like below example:
                // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
                if (authorizationHeader != null) {
                    authorizationHeader = authorizationHeader.trim();
                }

                // Validates token string to see if it is a valid access token, and it does not expire yet.
                ParsedToken parsedToken = checkAuthorizationHeader(authorizationHeader);

                // If token is valid we still need to check if it is still active.
                if (parsedToken.valid) {
                    parsedToken.valid = activeTokenManager.isActiveAccessToken(parsedToken.claims.username, parsedToken.claims.deviceId, parsedToken.token);
                }

                // Whatever token is valid or not we still need to check on url access permission.
                granted = checkUrlOnRequestMap(requestMethod, requestContextPath, urlFullPath, parsedToken.claims);

                // If user was authenticated but token is no longer valid or user is not granted for current url 401 code will be sent.
                if ((parsedToken.claims != null && !parsedToken.valid) || !granted) {
                    granted = false;
                }
            }
        }

        return granted;
    }

    /**
     * Checks on authorization header and tris to extract the claims.
     *
     * @param authorizationHeader: String of authorization header. Example: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
     */
    public ParsedToken checkAuthorizationHeader(String authorizationHeader) {
        ParsedToken result = new ParsedToken();

        if (!SharedUtil.isNullOrEmpty(authorizationHeader)) {
            // Fields splits the string authorizationHeader around each instance of one or more consecutive white space characters.
            String[] fields = authorizationHeader.split("\\s");

            // Checks the string if it contains bearer and token.
            if (fields.length == 2) {
                // Gets token string.
                String token = fields[1];

                // Validates token string to see if it is a valid access token, and it does not expire yet.
                result = tokenManager.validateToken(token, env.getProperty("jwt.signingKey"));
                result.token = token;
            }
        }

        return result;
    }

    /**
     * Checks requested url on security config request map.
     *
     * @param requestMethod String of http request method. Such like get, post.
     * @param contextPath   String of url root. Example: /web-api
     * @param url           String of full url. Example: /web-api/user/get
     * @param claims        Claims data.
     * @return True means request granted. False means request denied.
     */
    public boolean checkUrlOnRequestMap(String requestMethod, String contextPath, String url, MyClaims claims) {
        String urlWithoutRoot;
        String configuredUrl;
        boolean granted = false;

        // Removes root url.
        // For example, url is /family-expense/api/user/get.
        // Url without root is /user/get.
        urlWithoutRoot = url.replace(contextPath, "").replace("/" + apiPathPrefix, "");

        String tempUrl;

        for (RequestMap item : requestMapManager.getCachedRequestMaps()) {
            // If http request method is configured, and it does not match then just ignore.
            if (!SharedUtil.isNullOrEmpty(item.getHttpMethod()) && !requestMethod.equalsIgnoreCase(item.getHttpMethod())) {
                continue;
            }

            // First of all we need to remove trailing wildcard.
            // For example, item.Url is /user/** or /user/*.
            // configuredUrl is /user.
            configuredUrl = item.getUrl().replace("/**", "");
            configuredUrl = configuredUrl.replace("/*", "");

            // Removes the configuredUrl from urlWithoutRoot.
            // Before removing, /user/get.
            // After removing, /get.
            tempUrl = urlWithoutRoot.replace(configuredUrl, "");

            if (tempUrl.isEmpty()) {
                // If tempUrl is empty that means the url is exactly matched with configuredUrl.
                // Then we need to check on item.ConfigAttribute.
                granted = checkUrlOnConfigAttribute(claims, item.getConfigAttribute());

                // If we find exactly match we just need to stop.
                break;
            }

            if (tempUrl.equals(urlWithoutRoot)) {
                // If configuredUrl is /user/status then tempUrl will be the same as urlWithoutRoot.
                // That means a mismatch. We need to check on next.
                continue;
            }

            if (!tempUrl.startsWith("/")) {
                // If tempUrl does NOT start with "/" that means the parent url does not fully match.
                // For example
                // configuredUrl: /measuringData
                // urlWithoutRoot: /measuringDataSharing/queryValidDetails
                // tempUrl will be Sharing/queryValidDetails
                continue;
            }

            // Checks if url starts with tempUrl.
            // For example /authentication is configured but NOT /api/authentication.
            // We only check wildcard config if url ends with tempUrl.
            if (!urlWithoutRoot.startsWith(tempUrl)) {
                if (item.getUrl().endsWith(RequestMapManager.URL_DOUBLE_WILDCARD)) {
                    // If tempUrl is /get or /status/check it would also match the double wildcard.
                    // Then we need to check on item.ConfigAttribute.
                    granted = checkUrlOnConfigAttribute(claims, item.getConfigAttribute());
                } else if (item.getUrl().endsWith(RequestMapManager.URL_SINGLE_WILDCARD)) {
                    // If tempUrl is /get it can match the single wildcard. But /status/check does NOT.
                    // Then we need to check on item.ConfigAttribute.
                    if (tempUrl.contains("/")) {
                        granted = checkUrlOnConfigAttribute(claims, item.getConfigAttribute());
                    }
                }
            }
        }

        return granted;
    }

    /**
     * Checks requested url access permission on security configAttribute.
     * This would check on authentication and authorization.
     */
    private boolean checkUrlOnConfigAttribute(MyClaims claims, String configAttribute) {
        boolean granted = false;

        if (configAttribute.startsWith(RequestMapManager.PERMIT_ALL)) {
            granted = true;
        } else if (configAttribute.startsWith(RequestMapManager.IS_AUTHENTICATED_PREFIX) && claims != null) {
            granted = true;
        } else if ((configAttribute.startsWith(RequestMapManager.HAS_ROLE_PREFIX) ||
                configAttribute.startsWith(RequestMapManager.HAS_ANY_ROLE_PREFIX)) && claims != null) {
            granted = requestMapManager.containsAnyRoleInConfigAttribute(claims.roles, configAttribute);
        } else if ((configAttribute.startsWith(RequestMapManager.HAS_NO_ROLE_PREFIX) ||
                configAttribute.startsWith(RequestMapManager.HAS_NO_ANY_ROLE_PREFIX)) && claims != null) {
            granted = !requestMapManager.containsAnyRoleInConfigAttribute(claims.roles, configAttribute);
        }

        return granted;
    }
}
